using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.Threading;

namespace Attach.Internal
{
    public static class Interceptor
    {
        public static T GenerateObject<T>(Type[] constructorParameterTypes, object[] constructorParameters)
        {
            AssemblyName assemblyName = new AssemblyName();
            assemblyName.Name = "InterceptedClasses";
            AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder module;
            module = assemblyBuilder.DefineDynamicModule("InterceptedClasses.dll");

            Type wrapperType = GenerateWrapperType(typeof(T), module);

            //assemblyBuilder.Save(@"InterceptedClasses.dll");

            List<Type> parameterTypes = new List<Type>();
            parameterTypes.Add(typeof(Behaviors));
            bool nullParameterType = false;
            foreach (Type parameterType in constructorParameterTypes)
            {
                parameterTypes.Add(parameterType);
                if (parameterType == null)
                    nullParameterType = true;
            }

            ConstructorInfo ci = null;
            if (!nullParameterType)
            {
                ci = wrapperType.GetConstructor(parameterTypes.ToArray());
            }
            else
            {
                // Check for match by count only
                ConstructorInfo[] constructors = wrapperType.GetConstructors();
                foreach (ConstructorInfo constructor in wrapperType.GetConstructors())
                {
                    if (constructor.GetParameters().Length == parameterTypes.Count)
                    {
                        if (ci == null)
                        {
                            ci = constructor;
                        }
                        else
                        {
                            throw new Exception("Multiple constructors match parameter count, cannot determine correct one because of null parameter");
                        }
                    }
                }
            }
            if (ci == null)
            {
                throw new Exception("Could not find matching constructor");
            }

            List<Object> parameters = new List<object>();
            parameters.Add(new Behaviors());
            foreach (object parameter in constructorParameters)
            {
                parameters.Add(parameter);
            }
            T obj = (T)ci.Invoke(parameters.ToArray());

            return obj;
        }

        public static T GenerateObject<T>(params object[] constructorParameters)
        {
            object[] parameters = constructorParameters;
            if (!(constructorParameters is object[]))
            {
                parameters = new object[] { constructorParameters };
            }
            List<Type> parameterTypes = new List<Type>();
            foreach (object parameter in parameters)
            {
                if (parameter == null)
                    parameterTypes.Add(null);
                else
                    parameterTypes.Add(parameter.GetType());
            }
            return GenerateObject<T>(parameterTypes.ToArray(), parameters);
        }

        private static Type GenerateWrapperType(Type targetType, ModuleBuilder module)
        {
            TypeBuilder typeBuilder = module.DefineType(targetType.Name + "Wrapper", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, targetType);

            MethodBuilder setupBehaviorsMethod = typeBuilder.DefineMethod("SetupBehaviors", MethodAttributes.Private | MethodAttributes.HideBySig, null, new Type[] { typeof(Behaviors) });
            ILGenerator il = setupBehaviorsMethod.GetILGenerator();
            il.Emit(OpCodes.Nop);

            foreach (MethodInfo methodInfo in targetType.GetMethods())
            {
                if (methodInfo.IsPublic && methodInfo.IsVirtual && !methodInfo.IsFinal)
                {
                    string methodName = methodInfo.Name;
                    FieldBuilder behaviors = typeBuilder.DefineField(methodName + "Behaviors", typeof(ContinueMethod), FieldAttributes.Private);

                    GenerateOverloadMethod(methodInfo, typeBuilder, behaviors);
                    MethodBuilder targetMethod = GenerateTargetMethod(methodInfo, typeBuilder, targetType);

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Ldstr, methodName);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldftn, targetMethod.GetBaseDefinition());
                    il.Emit(OpCodes.Newobj, typeof(ContinueMethod).GetConstructors()[0]);
                    il.Emit(OpCodes.Callvirt, typeof(Behaviors).GetMethod("GetBehaviorsMethod"));
                    il.Emit(OpCodes.Stfld, behaviors);
                }
            }
            il.Emit(OpCodes.Ret);

            GenerateConstructors(typeBuilder, targetType, setupBehaviorsMethod.GetBaseDefinition());

            return typeBuilder.CreateType();
        }

        private static void GenerateConstructors(TypeBuilder typeBuilder, Type targetType, MethodInfo setupBehaviorsMethod)
        {
            ConstructorInfo[] constructors = targetType.GetConstructors();
            foreach (ConstructorInfo constructor in constructors)
            {
                if (constructor.IsPublic)
                {
                    List<Type> parameterTypes = new List<Type>();
                    parameterTypes.Add(typeof(Behaviors));
                    foreach (ParameterInfo parameterInfo in constructor.GetParameters())
                    {
                        parameterTypes.Add(parameterInfo.ParameterType);
                    }
                    ConstructorBuilder wrappedConstructor = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.HasThis, parameterTypes.ToArray());
                    ILGenerator il = wrappedConstructor.GetILGenerator();
                    il.Emit(OpCodes.Ldarg_0);
                    for (int i = 0; i < constructor.GetParameters().Length; i++)
                    {
                        switch (i)
                        {
                            case 0:
                                il.Emit(OpCodes.Ldarg_2);
                                break;
                            case 1:
                                il.Emit(OpCodes.Ldarg_3);
                                break;
                            default:
                                il.Emit(OpCodes.Ldarg_S, i + 2);
                                break;
                        }
                    }
                    il.Emit(OpCodes.Call, constructor);
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Call, setupBehaviorsMethod);
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Ret);
                }
            }
        }

        private static void GenerateOverloadMethod(MethodInfo methodInfo, TypeBuilder typeBuilder, FieldBuilder behaviors)
        {
            string methodName = methodInfo.Name;
            List<Type> parameterTypes = new List<Type>();
            foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
            {
                parameterTypes.Add(parameterInfo.ParameterType);
            }
            MethodBuilder method = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, methodInfo.ReturnType, parameterTypes.ToArray());
            ILGenerator il = method.GetILGenerator();
            if (methodInfo.ReturnType != typeof(void))
            {
                il.DeclareLocal(methodInfo.ReturnType);
            }
            if (methodInfo.GetParameters().Length > 0)
            {
                il.DeclareLocal(typeof(object[]));
            }
            il.Emit(OpCodes.Nop);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, behaviors);
            switch (methodInfo.GetParameters().Length)
            {
                case 0:
                    il.Emit(OpCodes.Ldc_I4_0);
                    break;
                case 1:
                    il.Emit(OpCodes.Ldc_I4_1);
                    break;
                case 2:
                    il.Emit(OpCodes.Ldc_I4_2);
                    break;
                case 3:
                    il.Emit(OpCodes.Ldc_I4_3);
                    break;
                case 4:
                    il.Emit(OpCodes.Ldc_I4_4);
                    break;
                case 5:
                    il.Emit(OpCodes.Ldc_I4_5);
                    break;
                case 6:
                    il.Emit(OpCodes.Ldc_I4_6);
                    break;
                case 7:
                    il.Emit(OpCodes.Ldc_I4_7);
                    break;
                case 8:
                    il.Emit(OpCodes.Ldc_I4_8);
                    break;
                default:
                    il.Emit(OpCodes.Ldc_I4_S, methodInfo.GetParameters().Length);
                    break;
            }
            il.Emit(OpCodes.Newarr, typeof(object));
            if (methodInfo.GetParameters().Length > 0)
            {
                if (methodInfo.ReturnType == typeof(void))
                {
                    il.Emit(OpCodes.Stloc_0);
                    il.Emit(OpCodes.Ldloc_0);
                }
                else
                {
                    il.Emit(OpCodes.Stloc_1);
                    il.Emit(OpCodes.Ldloc_1);
                }
                for (int i = 0; i < methodInfo.GetParameters().Length; i++)
                {
                    switch (i)
                    {
                        case 0:
                            il.Emit(OpCodes.Ldc_I4_0);
                            break;
                        case 1:
                            il.Emit(OpCodes.Ldc_I4_1);
                            break;
                        case 2:
                            il.Emit(OpCodes.Ldc_I4_2);
                            break;
                        case 3:
                            il.Emit(OpCodes.Ldc_I4_3);
                            break;
                        case 4:
                            il.Emit(OpCodes.Ldc_I4_4);
                            break;
                        case 5:
                            il.Emit(OpCodes.Ldc_I4_5);
                            break;
                        case 6:
                            il.Emit(OpCodes.Ldc_I4_6);
                            break;
                        case 7:
                            il.Emit(OpCodes.Ldc_I4_7);
                            break;
                        case 8:
                            il.Emit(OpCodes.Ldc_I4_8);
                            break;
                        default:
                            il.Emit(OpCodes.Ldc_I4_S, i);
                            break;
                    }
                    switch (i)
                    {
                        case 0:
                            il.Emit(OpCodes.Ldarg_1);
                            break;
                        case 1:
                            il.Emit(OpCodes.Ldarg_2);
                            break;
                        case 2:
                            il.Emit(OpCodes.Ldarg_3);
                            break;
                        default:
                            il.Emit(OpCodes.Ldarg_S, i + 1);
                            break;
                    }
                    if (!methodInfo.GetParameters()[i].ParameterType.IsClass)
                    {
                        il.Emit(OpCodes.Box, methodInfo.GetParameters()[i].ParameterType);
                    }
                    il.Emit(OpCodes.Stelem_Ref);
                    if (methodInfo.ReturnType == typeof(void))
                    {
                        il.Emit(OpCodes.Ldloc_0);
                    }
                    else
                    {
                        il.Emit(OpCodes.Ldloc_1);
                    }
                }
            }
            il.Emit(OpCodes.Callvirt, typeof(ContinueMethod).GetMethod("Invoke"));
            if (methodInfo.ReturnType == typeof(void))
            {
                il.Emit(OpCodes.Pop);
            }
            else
            {
                if (methodInfo.ReturnType.IsClass)
                {
                    il.Emit(OpCodes.Castclass, methodInfo.ReturnType);
                }
                else
                {
                    il.Emit(OpCodes.Unbox_Any, methodInfo.ReturnType);
                }
                il.Emit(OpCodes.Stloc_0);
                Label end = il.DefineLabel();
                il.Emit(OpCodes.Br_S, end);
                il.MarkLabel(end);
                il.Emit(OpCodes.Ldloc_0);
            }
            il.Emit(OpCodes.Ret);
        }

        private static MethodBuilder GenerateTargetMethod(MethodInfo methodInfo, TypeBuilder typeBuilder, Type targetType)
        {
            string methodName = methodInfo.Name;
            MethodBuilder targetMethod = typeBuilder.DefineMethod(methodName + "Target", MethodAttributes.Public | MethodAttributes.HideBySig, typeof(object), new Type[] { typeof(object[]) });
            ILGenerator il = targetMethod.GetILGenerator();
            il.DeclareLocal(typeof(object));
            il.Emit(OpCodes.Nop);
            il.Emit(OpCodes.Ldarg_0);
            if (methodInfo.GetParameters().Length > 0)
            {
                for (int i = 0; i < methodInfo.GetParameters().Length; i++)
                {
                    il.Emit(OpCodes.Ldarg_1);
                    switch (i)
                    {
                        case 0:
                            il.Emit(OpCodes.Ldc_I4_0);
                            break;
                        case 1:
                            il.Emit(OpCodes.Ldc_I4_1);
                            break;
                        case 2:
                            il.Emit(OpCodes.Ldc_I4_2);
                            break;
                        case 3:
                            il.Emit(OpCodes.Ldc_I4_3);
                            break;
                        case 4:
                            il.Emit(OpCodes.Ldc_I4_4);
                            break;
                        case 5:
                            il.Emit(OpCodes.Ldc_I4_5);
                            break;
                        case 6:
                            il.Emit(OpCodes.Ldc_I4_6);
                            break;
                        case 7:
                            il.Emit(OpCodes.Ldc_I4_7);
                            break;
                        case 8:
                            il.Emit(OpCodes.Ldc_I4_8);
                            break;
                        default:
                            il.Emit(OpCodes.Ldc_I4_S, i);
                            break;
                    }
                    il.Emit(OpCodes.Ldelem_Ref);
                    if (methodInfo.GetParameters()[i].ParameterType.IsClass)
                    {
                        il.Emit(OpCodes.Castclass, methodInfo.GetParameters()[i].ParameterType);
                    }
                    else
                    {
                        il.Emit(OpCodes.Unbox_Any, methodInfo.GetParameters()[i].ParameterType);
                    }
                }
            }
            il.Emit(OpCodes.Call, methodInfo);
            if (methodInfo.ReturnType == typeof(void))
            {
                il.Emit(OpCodes.Nop);
                il.Emit(OpCodes.Ldnull);
            }
            else if (!methodInfo.ReturnType.IsClass)
            {
                il.Emit(OpCodes.Box, methodInfo.ReturnType);
            }
            il.Emit(OpCodes.Stloc_0);
            Label end = il.DefineLabel();
            il.Emit(OpCodes.Br_S, end);
            il.MarkLabel(end);
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ret);

            return targetMethod;
        }
    }
}
